package com.dbflow5.processor.utils;

import com.dbflow5.processor.ProcessorManager;
import com.squareup.javapoet.ClassName;
import com.squareup.javapoet.TypeName;
import javax.annotation.processing.ProcessingEnvironment;
import javax.lang.model.element.Element;
import javax.lang.model.element.Modifier;
import javax.lang.model.element.TypeElement;
import javax.lang.model.type.MirroredTypeException;
import javax.lang.model.type.TypeMirror;
import javax.tools.Diagnostic;
import java.lang.annotation.Annotation;
import java.util.function.Function;

/**
 * Whether the specified element implements the [ClassName]
 */
public class ProcessorUtils{
    public static boolean implementsClass(TypeElement element, ProcessingEnvironment processingEnvironment, ClassName className) {
        return implementsClass(element, processingEnvironment, className.toString());
    }

    /**
     * Whether the specified element is assignable to the fqTn parameter

     * @param processingEnvironment The environment this runs in
     * *
     * @param fqTn                  THe fully qualified type name of the element we want to check
     * *
     * @param element               The element to check that implements
     * *
     * @return true if element implements the fqTn
     */
    public static boolean implementsClass(TypeElement element, ProcessingEnvironment processingEnvironment, String fqTn) {
        TypeElement typeElement = processingEnvironment.getElementUtils().getTypeElement(fqTn);
        if (typeElement == null) {
            processingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR,
                    "Type Element was null for: "+fqTn+" ensure that the visibility of the class is not private.");
            return false;
        } else {
            TypeMirror classMirror = ElementExtensions.erasure(typeElement.asType(), ProcessorManager.manager);
            if (classMirror == null || element.asType() == null) {
                return false;
            }
            TypeMirror elementType = element.asType();
            return elementType != null && (processingEnvironment.getTypeUtils().isAssignable(elementType, classMirror) || elementType == classMirror);
        }
    }

    /**
     * Whether the specified element is assignable to the [className] parameter
     *
     * @param element element
     * @param processingEnvironment processingEnvironment
     * @param className className
     * @return is subclass
     */
    public static boolean isSubclass(TypeElement element, ProcessingEnvironment processingEnvironment, ClassName className) {
        return isSubclass(element, processingEnvironment, className.toString());
    }

    /**
     * Whether the specified element is assignable to the [fqTn] parameter
     *
     * @param element element
     * @param processingEnvironment processingEnvironment
     * @param fqTn fqTn
     * @return is subclass
     */
    public static boolean isSubclass(TypeElement element, ProcessingEnvironment processingEnvironment, String fqTn) {
        TypeElement typeElement = processingEnvironment.getElementUtils().getTypeElement(fqTn);
        if (typeElement == null) {
            processingEnvironment.getMessager().printMessage(Diagnostic.Kind.ERROR, "Type Element was null for: "+fqTn+" ensure that the visibility of the class is not private.");
            return false;
        } else {
            TypeMirror classMirror = typeElement.asType();
            return classMirror != null && element != null && element.asType() != null && processingEnvironment.getTypeUtils().isSubtype(element.asType(), classMirror);
        }
    }

    public static ClassName fromTypeMirror(TypeMirror typeMirror, ProcessorManager processorManager) {
        TypeElement element = getTypeElement(typeMirror);
        if (element != null) {
            return ClassName.get(element);
        } else {
            return ElementUtility.getClassName(typeMirror.toString(), processorManager);
        }
    }

    public static TypeElement getTypeElement(Element element) {
        if(element instanceof TypeElement) {
            return (TypeElement)element;
        }else {
            return getTypeElement(element.asType());
        }
    }

    public static TypeElement getTypeElement(TypeMirror typeMirror) {
        ProcessorManager manager = ProcessorManager.manager;
        TypeElement typeElement = ElementExtensions.toTypeElement(typeMirror, manager);
        if (typeElement == null) {
            Element el = manager.typeUtils.asElement(typeMirror);
            if (el instanceof TypeElement) {
                typeElement = (TypeElement)el;
            }else {
                typeElement = null;
            }
        }
        return typeElement;
    }

    public static void ensureVisibleStatic(Element element, TypeElement typeElement, String name) {
        if (element.getModifiers().contains(Modifier.PRIVATE) || element.getModifiers().contains(Modifier.PROTECTED)) {
            ProcessorManager.manager.logError("$name must be visible from: " + typeElement);
        }
        if (!element.getModifiers().contains(Modifier.STATIC)) {
            ProcessorManager.manager.logError("$name must be static from: " + typeElement);
        }

        if (!element.getModifiers().contains(Modifier.FINAL)) {
            ProcessorManager.manager.logError("The $name must be final");
        }
    }

    public static <A extends Annotation> TypeName extractTypeNameFromAnnotation(Element element, Class<A> clazz, Function<A, Void> invoker) {
        A a = ElementExtensions.annotation(clazz, element);
        try {
            invoker.apply(a);
        } catch (MirroredTypeException mte) {
            return TypeName.get(mte.getTypeMirror());
        }
        return null;
    }

    public static <A extends Annotation> TypeMirror extractTypeMirrorFromAnnotation(A a, Function<MirroredTypeException, Void> exceptionHandler, Function<A, Void> invoker) {
        TypeMirror mirror = null;
        try {
            invoker.apply(a);
        } catch (MirroredTypeException mte) {
            exceptionHandler.apply(mte);
            mirror = mte.getTypeMirror();
        }
        return mirror;
    }

    public static <A extends Annotation> TypeName extractTypeNameFromAnnotation(A a, Function<MirroredTypeException, Void> exceptionHandler, Function<A, Void> invoker) {
        return TypeName.get(extractTypeMirrorFromAnnotation(a, exceptionHandler, invoker));
    }
}